/*
 * Copyright (C) 2013 Allwinnertech, kevin.z.m <kevin@allwinnertech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */
#include <linux/clk-private.h>
#include <linux/clk-provider.h>
#include <linux/clkdev.h>
#include <linux/clk/sunxi.h>
#include <mach/sys_config.h>
#include "clk-sunxi.h"
#include "clk-factors.h"
#include "clk-periph.h"
#ifdef CONFIG_ARCH_SUN9IW1
#include "clk-sun9iw1.h"
#endif
#ifdef CONFIG_ARCH_SUN8IW6
#include "clk-sun8iw6.h"
#endif
#include <linux/arisc/arisc.h>

#define CK32K_OUT_CTRL1  0xC1
#define CK32K_OUT_CTRL2  0xC2
#define CK32K_OUT_CTRL3  0xC3

/*
SUNXI_CLK_PERIPH(name,       mux_reg,         mux_shift, mux_width, div_reg,        div_mshift, div_mwidth, div_nshift, div_nwidth, gate_flags, enable_reg,     reset_reg, bus_gate_reg, drm_gate_reg, enable_shift, reset_shift, bus_gate_shift, dram_gate_shift,lock,com_gate,com_gate_off)
*/
SUNXI_CLK_PERIPH(ac10032k1, CK32K_OUT_CTRL1, 4,         1,         CK32K_OUT_CTRL1,5,          3,          1,          3,          0,          CK32K_OUT_CTRL1,0,         0,            0,            0,            0,           0,              0,              NULL,NULL,             0);
SUNXI_CLK_PERIPH(ac10032k2, CK32K_OUT_CTRL2, 4,         1,         CK32K_OUT_CTRL2,5,          3,          1,          3,          0,          CK32K_OUT_CTRL2,0,         0,            0,            0,            0,           0,              0,              NULL,NULL,             0);
SUNXI_CLK_PERIPH(ac10032k3, CK32K_OUT_CTRL3, 4,         1,         CK32K_OUT_CTRL3,5,          3,          1,          3,          0,          CK32K_OUT_CTRL3,0,         0,            0,            0,            0,           0,              0,              NULL,NULL,             0);
static const char *ac10032k_parents[]  = {"32k_rtc", "4m_adda"};
static struct periph_init_data sunxi_ac100_init[] = {
	{"ac10032k1", CLK_GET_RATE_NOCACHE,ac10032k_parents,     ARRAY_SIZE(ac10032k_parents),     &sunxi_clk_periph_ac10032k1},
	{"ac10032k2", CLK_GET_RATE_NOCACHE,ac10032k_parents,     ARRAY_SIZE(ac10032k_parents),     &sunxi_clk_periph_ac10032k2},
	{"ac10032k3", CLK_GET_RATE_NOCACHE,ac10032k_parents,     ARRAY_SIZE(ac10032k_parents),     &sunxi_clk_periph_ac10032k3},
};

static unsigned int ac100_m_factor[]={1,2,4,8,16,32,64,122};
static unsigned int ac100_n_factor[]={1,2,4,8,16,32,64,122};

static unsigned long sunxi_ac100_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
{
	unsigned long reg;
	struct sunxi_clk_periph *periph = to_clk_periph(hw);
	struct sunxi_clk_periph_div *divider = &periph->divider;
	unsigned long div, div_m = 0, div_n = 0;
	u64 rate = parent_rate;
	if(!divider->reg)
		return parent_rate;

	reg = periph_readl(periph,divider->reg);
	if(divider->mwidth)
		div_m = GET_BITS(divider->mshift, divider->mwidth, reg);
	if(divider->nwidth)
		div_n = GET_BITS(divider->nshift, divider->nwidth, reg);
	if(reg & 0x100)
		div = ac100_m_factor[div_m]*ac100_n_factor[div_n];
	else
		div = ac100_n_factor[div_n];
	do_div(rate, div);
	return rate;
}
static long sunxi_ac100_round_rate(struct clk_hw *hw, unsigned long rate, unsigned long *prate)
{
	int i,j,m_max;
	int m=0,n=0;
	unsigned long cur_rate=0,new_rate=0;
	unsigned long cur_delta,new_delta;
	u32 parent_rate = *prate;
	if(*prate == 4000000)
		m_max =1;
	else
		m_max = 8;

	for(i=0;i<m_max;i++)
	for(j=0;j<8;j++)
	{
		new_rate = parent_rate/(ac100_m_factor[i]*ac100_n_factor[j]);
		new_delta = (new_rate >rate)?(new_rate-rate):(rate-new_rate);
		cur_delta = (cur_rate >rate)?(cur_rate-rate):(rate-cur_rate);
		if(new_delta < cur_delta)
		{
			cur_rate = new_rate;
			m =i;
			n = j;
		}
	}

	return cur_rate;
}
static int __sunxi_clk_periph_set_rate(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate)
{
	struct sunxi_clk_periph *periph = to_clk_periph(hw);
	struct sunxi_clk_periph_div *divider = &periph->divider;
	int i,j,m_max;
	int m=0,n=0;
	unsigned long cur_rate=0,new_rate=0;
	unsigned long cur_delta,new_delta;
	u32 reg;
	if(parent_rate == 4000000)
		m_max =1;
	else
		m_max = 8;
	for(i=0;i<m_max;i++)
	for(j=0;j<8;j++)
	{
		new_rate = parent_rate/(ac100_m_factor[i]*ac100_n_factor[j]);
		new_delta = (new_rate >rate)?(new_rate-rate):(rate-new_rate);
		cur_delta = (cur_rate >rate)?(cur_rate-rate):(rate-cur_rate);
		if(new_delta < cur_delta)
		{
			cur_rate = new_rate;
			m =i;
			n = j;
		}
	}
	reg = periph_readl(periph,divider->reg);
	if(divider->mwidth)
		reg = SET_BITS(divider->mshift, divider->mwidth, reg, m);
	if(divider->nwidth)
		reg = SET_BITS(divider->nshift, divider->nwidth, reg, n);
	periph_writel(periph,reg, divider->reg);
	return 0;
}

static u32 ac100_readl(void __iomem * reg)
{
	arisc_rsb_block_cfg_t rsb_data;
	unsigned char addr;
	unsigned int val;

	addr = (unsigned char)((unsigned long)reg);
	rsb_data.len = 1;
	rsb_data.datatype = RSB_DATA_TYPE_HWORD;
	rsb_data.msgattr = ARISC_MESSAGE_ATTR_SOFTSYN;
	rsb_data.devaddr = RSB_RTSADDR_AC100;
	rsb_data.regaddr =  &addr;
	rsb_data.data = &val;
	/* read registers */
	if (arisc_rsb_read_block_data(&rsb_data))
		pr_err("%s(%d) err: read reg-0x%x failed", __func__, __LINE__, (unsigned int __force)reg);
	return val;
}

static void ac100_writel(u32 val,void __iomem * reg)
{
	arisc_rsb_block_cfg_t rsb_data;
	u16 data = (u16)val;

	rsb_data.len = 1;
	rsb_data.datatype = RSB_DATA_TYPE_HWORD;
	rsb_data.msgattr = ARISC_MESSAGE_ATTR_SOFTSYN;
	rsb_data.devaddr = RSB_RTSADDR_AC100;
	rsb_data.regaddr = (unsigned char *)&reg;
	rsb_data.data = (unsigned int *)&data;

#ifdef CONFIG_ARCH_SUN9IW1
	if(((unsigned int __force)reg == 0xc1) && (!(val&0x01)))
	{
		pr_err("Warning!!! %s skip write %x to reg %x\n", __func__,val,(unsigned int __force)reg);
		return;
	}
#endif
	/* write registers */
	if (arisc_rsb_write_block_data(&rsb_data))
		pr_err("%s(%d) err: write reg-0x%x failed", __func__, __LINE__, (unsigned int __force)reg);
	return;
}
static struct clk_ops ac100_clkops;
static struct sunxi_reg_ops ac100_regops;
extern void sunxi_clk_get_periph_ops(struct clk_ops* ops);

static int __init sunxi_init_ac100_clocks(void)
{
	struct clk *clk;
	struct clk *parent;
	int i;
	struct periph_init_data *pd;

	if (arisc_rsb_set_rtsaddr(RSB_DEVICE_SADDR7, RSB_RTSADDR_AC100)) {
		pr_err("%s err: config codec failed\n", __func__);
		return -1;
	}

	//reigster source for AC100 32K
	clk = clk_register_fixed_rate(NULL, "32k_rtc", NULL, CLK_IS_ROOT, 32768);
	clk_register_clkdev(clk, "32k_rtc", NULL);
	clk = clk_register_fixed_rate(NULL, "4m_adda", NULL, CLK_IS_ROOT, 4000000);
	clk_register_clkdev(clk, "4m_adda", NULL);

	sunxi_clk_get_periph_ops(&ac100_clkops);
	ac100_clkops.prepare = ac100_clkops.enable;
	ac100_clkops.unprepare = ac100_clkops.disable;
	ac100_clkops.enable = NULL;
	ac100_clkops.disable = NULL;
	ac100_clkops.recalc_rate = sunxi_ac100_recalc_rate;
	ac100_clkops.round_rate = sunxi_ac100_round_rate;
	ac100_clkops.set_rate = __sunxi_clk_periph_set_rate;
	ac100_regops.reg_writel = ac100_writel;
	ac100_regops.reg_readl = ac100_readl;
	/* register AC100 clock */
	for (i = 0; i < ARRAY_SIZE(sunxi_ac100_init); i++) {
		pd = &sunxi_ac100_init[i];
		pd->periph->priv_clkops = &ac100_clkops;
		pd->periph->priv_regops = &ac100_regops;
		clk = sunxi_clk_register_periph(pd,  NULL);
		clk_register_clkdev(clk, pd->name, NULL);
	}

	//Sync enable count for Ac100
	for(i=0; i<ARRAY_SIZE(sunxi_ac100_init); i++)
	{
		pd = &sunxi_ac100_init[i];
		clk = clk_get(NULL, pd->name);
		if(!clk || IS_ERR(clk))
			continue;
		if((!clk->prepare_count) && (!clk->enable_count) && clk->ops->is_enabled(clk->hw))
		{
			clk->prepare_count++;
			clk->enable_count++;
			parent = clk->parent;
			while(parent)
			{
				parent->enable_count++;
				parent->prepare_count++;
				parent = parent->parent;
			}
		}
		clk_put(clk);
	}
	return 0;
}
subsys_initcall_sync(sunxi_init_ac100_clocks);

